package com.csthink.auditlog.service;

import com.csthink.auditlog.domain.AuditLog;
import com.csthink.auditlog.domain.ScrollAuditLogs;
import com.csthink.auditlog.dto.AuditLogQueryParamDTO;
import com.csthink.auditlog.dto.PageResultDTO;
import com.csthink.auditlog.repository.AuditLogRepository;
import com.csthink.common.infrastructure.util.I18nUtil;
import com.csthink.common.infrastructure.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;

import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author <a href="mailto:security.2009@live.cn">Mars</a>
 * @since 2023-09-12
 */
@Slf4j
public abstract class AbstractAuditLogService {

    protected AuditLogRepository auditLogRepository;

    /**
     * LinkedBlockingQueue may block when put Element into the Queue, but
     * blocking only happens when the queue is full. LinkedBlockingQueue's
     * capacity is equal to Integer.MAX_VALUE. It's big enough.
     * <p>
     * In most case, it will not block, we can just use it.
     */
    private final BlockingQueue<AuditLog> auditLogQueue = new LinkedBlockingQueue<>();


    protected AuditLog save(AuditLog auditLog) throws IOException {
        return auditLogRepository.save(auditLog);
    }

    /**
     * Just put in a blocking queue, save logs in another thread.
     * In the saving thread, It uses batch save.
     */

    protected void asyncSave(AuditLog auditLog) {
        if (auditLog == null) return;
        putInQueue(auditLogQueue, auditLog);
        log.debug("Put Audit Log in queue, waiting for batch saving.");
        log.debug("Audit Log: {E}", auditLog.getSimpleInfo());

    }


    protected AuditLog findById(String id) throws IOException {
        return auditLogRepository.findById(id);
    }


    protected PageResultDTO<AuditLog> query(AuditLogQueryParamDTO queryParam) throws IOException {
        return auditLogRepository.query(queryParam);
    }


    protected ScrollAuditLogs scrollAuditLog(AuditLogQueryParamDTO queryParam) throws IOException {
        return auditLogRepository.scrollAuditLog(queryParam);
    }

    /**
     * Not logically complete, log how many audit logs haven't recorded.
     */
    @PreDestroy
    protected void checkIfAllSaved() {
        if (!auditLogQueue.isEmpty()) {
            log.warn(String.format("Still have %d Audit Logs not been saved.", auditLogQueue.size()));

            List<AuditLog> auditLogs = new ArrayList<>();
            auditLogQueue.drainTo(auditLogs, 50);
            log.warn(String.format("Head %d Audit Logs are blow:", auditLogs.size()));
            for (AuditLog al : auditLogs) {
                log.debug("Audit Log: {E}. has not been saved.", al.getSimpleInfo());
            }

        }
    }

    private void putInQueue(BlockingQueue<AuditLog> queue, AuditLog auditLog) {
        if (null == auditLog)
            return;
        boolean cached = false;
        while (!cached) {
            try {
                queue.put(auditLog);
                cached = true;
            } catch (InterruptedException e) {
                // continue redo put, until put success.
                // Thread interrupts rarely happen.
            }
        }
    }

    private ReentrantLock lock = new ReentrantLock();
    private long fails = 0L;
    private long failWaiting = 0L;
    private long lastTaskTime = 0L;

    protected void savingTask() {
        // If there is no another thread do saving, try to do;
        // Else just return.
        if (lock.tryLock()) {
            if (System.currentTimeMillis() - lastTaskTime > failWaiting * 1000) {
                saving();
            }
            lock.unlock();
        } else {
            log.warn("Audit Log saving task is running, waiting for next time.");
        }
    }

    private void saving() {
        List<AuditLog> auditLogs = new ArrayList<>();
        try {
            // Blocking until get a log;
            while (!auditLogQueue.isEmpty()) {
                auditLogs = new ArrayList<>();
                // Caution: for ES, a good starting point is 5–15 MB per bulk.
                // A single AuditLog's JSON is nearly 2K.
                // So, we use 2000 here.
                auditLogQueue.drainTo(auditLogs, 2000);
                if (auditLogs.isEmpty())
                    return;
                log.debug(String.format("Trying to batch saving %d Audit Logs", auditLogs.size()));
                auditLogRepository.batchSave(auditLogs);
                log.debug("Success to batch saving these audit logs.");
                for (AuditLog al : auditLogs) {
                    log.debug("Audit Log: {E}", al.getSimpleInfo());
                }
                // Clear fail status
                fails = 0;
                failWaiting = 0;
            }
        } catch (Exception e) {
            // When got here, es may not work correctly, So delay and
            // try again.
            if (fails < Long.MAX_VALUE)
                fails++;
            log.error("Fail to save audit log {} times, caused by: ", fails, e);
            for (AuditLog al : auditLogs)
                putInQueue(auditLogQueue, al);
            // wait for fails seconds. max 60s*3 = 3 min.
            failWaiting = fails > 7 ? 60 * 3 : 2 << fails;
            log.warn("Saving failed too much, waiting {} seconds.", failWaiting);
        }
        lastTaskTime = System.currentTimeMillis();
    }

//
//    protected String auditLogCsvExport(AuditLogQueryParam queryParam, User loginUser) {
//        String[] headers = getAuditLogCsvTitle();
//        File file = null;
//        String downloadUrl = "";
//        try {
//            String dateStr = DateUtils.formatDate(new Date(), "yyyy-MM-dd");
//            File tempFile = Files.createTempDir();
//            file = new File(tempFile.getAbsolutePath() + File.separator + "auditLogs_" + dateStr + ".csv");
//            if(!file.exists()){
//                file.createNewFile();
//            }
//            FileOutputStream fos = new FileOutputStream(file);
//            //Solve Chinese garbled characters
//            byte[] uft8bom={(byte)0xef,(byte)0xbb,(byte)0xbf};
//            fos.write(uft8bom);
//            CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(fos,StandardCharsets.UTF_8));
//            csvWriter.writeNext(headers);
//            List<String[]> contentList = getAuditLogContentList(queryParam);
//            csvWriter.writeAll(contentList);
//            csvWriter.flush();
//            String uniqueFilePath = fileServerService.uploadFile(file.getName(), file, loginUser.getAccountId(), true);
//            String encryptFileId = fileServerService.getEncryptFileId(uniqueFilePath);
//            downloadUrl = fileServerService.createFileDownloadLinkByFileId(encryptFileId, 30, TimeUnit.DAYS);
//            file.delete();
//
//            // auditlog
//            List<String> detail = Lists.newArrayList();
//            detail.add(I18nUtil.getMessage("alg.detail.operation.logs.export", new String[]{
//                    "auditLogs_" + dateStr + ".csv"
//            }));
//            detail.add("-" + I18nUtil.getMessage("alg.action.count") + ": " + contentList.size());
//            AuditLogBuilder.create(Source.CCI_WEB)
//                    .category(Category.LOGS)
//                    .action(Action.EXPORT)
//                    .subaction("Export auditLog")
//                    .detail(Joiner.on("\n").join(detail))
//                    .publish();
//        } catch (IOException e) {
//            if(file != null) {
//                file.delete();
//            }
//            log.error("user csv export error:{}", e);
//        }
//        return downloadUrl;
//    }

    private String[] getAuditLogCsvTitle() {
        String[] headers = new String[15];
        headers[0] = I18nUtil.getMessage("export.template.auditLog.time");
        headers[1] = I18nUtil.getMessage("export.template.auditLog.operator");
        headers[2] = I18nUtil.getMessage("export.template.auditLog.category");
        headers[3] = I18nUtil.getMessage("export.template.auditLog.action");
        headers[4] = I18nUtil.getMessage("export.template.auditLog.detail");
        return headers;
    }

    protected List<String[]> getAuditLogContentList(AuditLogQueryParamDTO queryParam) {
        List<String[]> resultList = new ArrayList<>();
        PageResultDTO<AuditLog> auditLogPageResultDto = new PageResultDTO<>();
        try {
            auditLogPageResultDto = auditLogRepository.query(queryParam);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        auditLogPageResultDto.getRecords().stream().forEach(auditLog -> {
            String[] content = new String[5];
            Date createTime = auditLog.getCreateTime();
            String dateStr = DateFormatUtils.format(createTime, "yyyy-MM-dd HH:mm:ss");
            String operatorInfo = auditLog.getOperatorInfo();
            String category = auditLog.getCategory();
            String action = auditLog.getAction();
            String detail = auditLog.getDetail();
            content[0] = StringUtil.getCSVStringWithProtection(dateStr);
            content[1] = StringUtil.getCSVStringWithProtection(operatorInfo);
            content[2] = StringUtil.getCSVStringWithProtection(category);
            content[3] = StringUtil.getCSVStringWithProtection(action);
            content[4] = StringUtil.getCSVStringWithProtection(detail);
            resultList.add(content);
        });
        return resultList;
    }


    protected int getAuditLogCount(AuditLogQueryParamDTO queryParam) {
        return 0;
    }
}
